/*
 * Copyright (C) 2011 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.util.concurrent;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static junit.framework.Assert.fail;

import com.google.common.testing.TearDown;
import com.google.common.testing.TearDownAccepter;

import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
 * Utilities for performing thread interruption in tests
 *
 * @author Kevin Bourrillion
 * @author Chris Povirk
 */
final class InterruptionUtil
{
    private static final Logger logger = Logger.getLogger(InterruptionUtil.class.getName());

    /**
     * Runnable which will interrupt the target thread repeatedly when run.
     */
    private static final class Interruptenator implements Runnable
    {
        private final long everyMillis;
        private final Thread interruptee;
        private volatile boolean shouldStop = false;

        Interruptenator(Thread interruptee, long everyMillis)
        {
            this.everyMillis = everyMillis;
            this.interruptee = interruptee;
        }

        @Override
        public void run()
        {
            while (true)
            {
                try
                {
                    Thread.sleep(everyMillis);
                }
                catch (InterruptedException e)
                {
                    // ok. just stop sleeping.
                }
                if (shouldStop)
                {
                    break;
                }
                interruptee.interrupt();
            }
        }

        void stopInterrupting()
        {
            shouldStop = true;
        }
    }

    /**
     * Interrupts the current thread after sleeping for the specified delay.
     */
    static void requestInterruptIn(final long time, final TimeUnit unit)
    {
        checkNotNull(unit);
        final Thread interruptee = Thread.currentThread();
        new Thread(
                new Runnable()
                {
                    @Override
                    public void run()
                    {
                        try
                        {
                            unit.sleep(time);
                        }
                        catch (InterruptedException wontHappen)
                        {
                            throw new AssertionError(wontHappen);
                        }
                        interruptee.interrupt();
                    }
                })
                .start();
    }

    static void repeatedlyInterruptTestThread(
            long interruptPeriodMillis, TearDownAccepter tearDownAccepter)
    {
        final Interruptenator interruptingTask =
                new Interruptenator(Thread.currentThread(), interruptPeriodMillis);
        final Thread interruptingThread = new Thread(interruptingTask);
        interruptingThread.start();
        tearDownAccepter.addTearDown(
                new TearDown()
                {
                    @Override
                    public void tearDown() throws Exception
                    {
                        interruptingTask.stopInterrupting();
                        interruptingThread.interrupt();
                        joinUninterruptibly(interruptingThread, 2500, MILLISECONDS);
                        Thread.interrupted();
                        if (interruptingThread.isAlive())
                        {
                            // This will be hidden by test-output redirection:
                            logger.severe("InterruptenatorTask did not exit; future tests may be affected");
                            /*
                             * This won't do any good under JUnit 3, but I'll leave it around in
                             * case we ever switch to JUnit 4:
                             */
                            fail();
                        }
                    }
                });
    }

    // TODO(cpovirk): promote to Uninterruptibles, and add untimed version
    private static void joinUninterruptibly(Thread thread, long timeout, TimeUnit unit)
    {
        boolean interrupted = false;
        try
        {
            long remainingNanos = unit.toNanos(timeout);
            long end = System.nanoTime() + remainingNanos;

            while (true)
            {
                try
                {
                    // TimeUnit.timedJoin() treats negative timeouts just like zero.
                    NANOSECONDS.timedJoin(thread, remainingNanos);
                    return;
                }
                catch (InterruptedException e)
                {
                    interrupted = true;
                    remainingNanos = end - System.nanoTime();
                }
            }
        }
        finally
        {
            if (interrupted)
            {
                Thread.currentThread().interrupt();
            }
        }
    }
}
